import { CACHE_MANAGER, CanActivate, ExecutionContext, Inject, Injectable, Logger } from '@nestjs/common'
import { ModuleRef, Reflector } from '@nestjs/core'
import { Cache } from 'cache-manager'
import { FastifyRequest } from 'fastify'
import { AUTH_ENABLED_KEY, AUTH_POLICY_KEY } from '../decorators/auth.decorator'
import { BoticordAbility } from '../casl/ability.type'
import { CaslAbilityFactory } from '../casl/casl-ability.factory'
import { JwtPayloadDto } from '../dto/jwt-payload.dto'
import { IPolicyHandler, PolicyHandler } from '../interfaces/policy-handler.interface'
import { Token } from '../models/token.model'
import { AuthService } from '../services/auth.service'

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    private readonly moduleRef: ModuleRef,
    private readonly auth: AuthService,
    @Inject(CACHE_MANAGER) private readonly cache: Cache,
    private readonly abilityFactory: CaslAbilityFactory,
  ) {}

  private readonly logger = new Logger(AuthGuard.name)

  private async extractToken(ctx: ExecutionContext) {
    if (ctx.getType() === 'http') {
      const token = ctx.switchToHttp().getRequest<FastifyRequest>().headers.authorization!

      return token ? this.auth.extractPayload(token) : null
    } else if (ctx.getType() === 'ws') {
      const jwt = ctx.switchToWs().getClient<{ jwt: JwtPayloadDto }>().jwt

      return jwt ?? null
    } else {
      return null
    }
  }

  private async resolveClass(policyHandler: PolicyHandler) {
    try {
      return this.moduleRef.get(policyHandler)
    } catch {
      return this.moduleRef.create(policyHandler)
    }
  }

  private async resolvePolicyHandler(context: ExecutionContext): Promise<IPolicyHandler | null> {
    const handlers = [ context.getHandler(), context.getClass() ]

    const enabled = this.reflector.getAllAndOverride<boolean>(AUTH_ENABLED_KEY, handlers)
    if (!enabled) {
      this.logger.verbose('disabled')
      return null
    }

    const policyHandlerVal = this.reflector.getAllAndOverride<PolicyHandler>(AUTH_POLICY_KEY, handlers)
    if (policyHandlerVal === undefined || policyHandlerVal === null) {
      this.logger.verbose('did not find policy handler')
      return null
    }

    return await this.resolveClass(policyHandlerVal)
  }

  private injectData(
    ctx: ExecutionContext, payload: JwtPayloadDto | undefined, perms: number, ability: BoticordAbility,
  ) {
    if (ctx.getType() === 'http') {
      const req = ctx.switchToHttp().getRequest<{ jwt?: JwtPayloadDto; perms: number; ability: BoticordAbility }>()
      req.jwt = payload
      req.perms = perms
      req.ability = ability
    } else if (ctx.getType() === 'ws') {
      const client = ctx.switchToWs().getClient<{ jwt?: JwtPayloadDto; perms: number; ability: BoticordAbility }>()
      client.jwt = payload
      client.perms = perms
      client.ability = ability
    }
  }
  
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const policyHandler = await this.resolvePolicyHandler(context)

    const jwtPayload = await this.extractToken(context)
    this.injectData(context, jwtPayload ? jwtPayload : undefined, 0, this.abilityFactory.forAnonymous())

    if (policyHandler === null) {
      this.logger.verbose('no policy handler - pass')
      return true
    }

    if (jwtPayload === null) {
      this.logger.verbose('anonymous')
      const anonymous = this.abilityFactory.forAnonymous()

      return policyHandler.handle(anonymous, context)
    }

    let token: Token | null

    if (jwtPayload.type === 'bot') {
      this.logger.verbose('bot token')
      token = await this.auth.getAutomationToken(jwtPayload.id)
    } else {
      this.logger.verbose('user token')
      token = await this.auth.getUserToken(jwtPayload.id)
    }

    if (token === null) {
      this.logger.verbose('token not found - not passing')
      return false
    }

    const ability = this.abilityFactory.forToken(token)

    this.injectData(context, jwtPayload, token.permissions, ability)
    return policyHandler.handle(ability, context, token)
  }
}
